/*
 *  This file is part of the Image Metadata Editor.
 *
 *  Copyright (C) 2015 Australian Institute of Marine Science
 *
 *  Contact: Gael Lafond <g.lafond@aims.org.au>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package au.gov.aims.exif.editor;

import java.io.UnsupportedEncodingException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * IMPORTANT
 * This class contains Unicode characters. It's in a separate file so the application can easily modified
 * without worrying about encoding, as long as this file is untouched. If you want to make modification
 * to this class, ensure your editor encoding is set properly and that the characters are displayed properly
 * before saving.
 */
public class UnicodeUtils {
	private static final Logger LOGGER = Logger.getLogger(UnicodeUtils.class.getName());

	// Those maps are used to convert Unicode characters to ISO 8859-1 and ASCII
	// ISO 8859-1 table:
	//     http://en.wikipedia.org/wiki/ISO/IEC_8859-1
	// Unicode characters encoding
	//     http://www.fileformat.info/info/unicode/char/search.htm
	//     NOTE: You can copy any unicode characters in the search to find it's code
	private static final Map<Character, String> UNICODE_TO_ISO_8859_1_CONVERSION_MAP = new HashMap<Character, String>();
	private static final Map<Character, String> ISO_8859_1_TO_ASCII_CONVERSION_MAP = new HashMap<Character, String>();
	static {
		// NOTE:
		// * ISO-8859-1: All characters with Unicode equal or lower than U+00FF are supported
		// * ASCII: All characters with Unicode equal or lower than U+007F are supported
		// * I used the unicode code for characters not supported by my editor (IntelliJ)


		// *************************
		// * UNICODE to ISO 8859-1 *
		// *************************

		// Map format:
		// Key: Unicode char
		// Value: ISO 8859-1 equivalence

		// Superscript
		// "¹", "²" and "³" are supported by ISO 8859-1
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('ʰ', "^h"); // MODIFIER LETTER SMALL H (U+02B0)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('⁰', "^0"); // SUPERSCRIPT ZERO (U+2070)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('⁴', "^4"); // SUPERSCRIPT FOUR (U+2074)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('⁵', "^5"); // SUPERSCRIPT FIVE (U+2075)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('⁶', "^6"); // SUPERSCRIPT SIX (U+2076)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('⁷', "^7"); // SUPERSCRIPT SEVEN (U+2077)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('⁸', "^8"); // SUPERSCRIPT EIGHT (U+2078)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('⁹', "^9"); // SUPERSCRIPT NINE (U+2079)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('ⁱ', "^i"); // SUPERSCRIPT LATIN SMALL LETTER I (U+2071)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('ⁿ', "^n"); // SUPERSCRIPT LATIN SMALL LETTER N (U+207F)

		// Subscript
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('₀', "0"); // SUBSCRIPT ZERO (U+2080)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('₁', "1"); // SUBSCRIPT ONE (U+2081)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('₂', "2"); // SUBSCRIPT TWO (U+2082)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('₃', "3"); // SUBSCRIPT THREE (U+2083)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('₄', "4"); // SUBSCRIPT FOUR (U+2084)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('₅', "5"); // SUBSCRIPT FIVE (U+2085)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('₆', "6"); // SUBSCRIPT SIX (U+2086)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('₇', "7"); // SUBSCRIPT SEVEN (U+2087)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('₈', "8"); // SUBSCRIPT EIGHT (U+2088)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('₉', "9"); // SUBSCRIPT NINE (U+2089)

		// Unicode control characters
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u00AD', ""); // SOFT HYPHEN (U+00AD)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u035C', ""); // COMBINING DOUBLE BREVE BELOW (U+035C)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u200B', ""); // ZERO WIDTH SPACE (U+200B)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u200C', ""); // ZERO WIDTH NON-JOINER (U+200C)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u200D', ""); // ZERO WIDTH JOINER (U+200D)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u200E', ""); // LEFT-TO-RIGHT MARK (U+200E)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u200F', ""); // RIGHT-TO-LEFT MARK (U+200F)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u2028', ""); // LINE SEPARATOR (U+2028)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u2029', ""); // PARAGRAPH SEPARATOR (U+2029)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u202A', ""); // LEFT-TO-RIGHT EMBEDDING (U+202A)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u202B', ""); // RIGHT-TO-LEFT EMBEDDING (U+202B)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u202C', ""); // POP DIRECTIONAL FORMATTING (U+202C)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u202D', ""); // LEFT-TO-RIGHT OVERRIDE (U+202D)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u202E', ""); // RIGHT-TO-LEFT OVERRIDE (U+202E)

		// Unicode spaces
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u2000', " "); // EN QUAD (U+2000)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u2001', "  "); // EM QUAD (U+2001)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u2002', " "); // EN SPACE (U+2002)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u2003', "  "); // EM SPACE (U+2003)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u2004', " "); // THREE-PER-EM SPACE (U+2004)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u2005', " "); // FOUR-PER-EM SPACE (U+2005)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u2006', " "); // SIX-PER-EM SPACE (U+2006)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u2007', " "); // FIGURE SPACE (U+2007)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u2008', " "); // PUNCTUATION SPACE (U+2008)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u2009', " "); // THIN SPACE (U+2009)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u200A', ""); // HAIR SPACE (U+200A)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u202F', " "); // NARROW NO-BREAK SPACE (U+202F)

		// Unicode dashes (normal dash: HYPHEN-MINUS (U+002D) => Hex: 2D)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('֊', "-"); // ARMENIAN HYPHEN (U+058A)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('‐', "-"); // HYPHEN (U+2010)              => Hex: E2 80 90
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('‑', "-"); // NON-BREAKING HYPHEN (U+2011) => Hex: E2 80 91
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('‒', "-"); // FIGURE DASH (U+2012)         => Hex: E2 80 92
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('–', "-"); // EN DASH (U+2013)             => Hex: E2 80 93
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('—', "-"); // EM DASH (U+2014)             => Hex: E2 80 94
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('―', "-"); // HORIZONTAL BAR (U+2015)      => Hex: E2 80 95
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('⁃', "-"); // HYPHEN BULLET (U+2043)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('⁻', "-"); // SUPERSCRIPT MINUS (U+207B)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('₋', "-"); // SUBSCRIPT MINUS (U+208B)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('﹣', "-"); // SMALL HYPHEN-MINUS (U+FE63)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('－', "-"); // FULLWIDTH HYPHEN-MINUS (U+FF0D)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('゠', "="); // KATAKANA-HIRAGANA DOUBLE HYPHEN (U+30A0)

		// Unicode quotes and coma
		// The grave accent or backtick (U+0060) "`" is supported by ASCII and ISO 8859-1 (no need to be present in this table)
		// NOTE: Those accents should not be used as quote, but it happen often.
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u0301', ""); // COMBINING ACUTE ACCENT (U+0301) (Sometime "é" is written using "e" + this strange character)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u0317', ""); // COMBINING ACUTE ACCENT BELOW (U+0317)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u02CB', "'"); // MODIFIER LETTER GRAVE ACCENT (U+02CB)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u02CE', ","); // MODIFIER LETTER LOW GRAVE ACCENT (U+02CE)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u02DF', "x"); // MODIFIER LETTER CROSS ACCENT (U+02DF)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u02FE', "'"); // MODIFIER LETTER MIDDLE GRAVE ACCENT (U+02F4)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u02F5', "\""); // MODIFIER LETTER MIDDLE DOUBLE GRAVE ACCENT (U+02F5)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u0300', ""); // COMBINING GRAVE ACCENT (U+0300)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u030F', ""); // COMBINING DOUBLE GRAVE ACCENT (U+030F)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u0316', ""); // COMBINING GRAVE ACCENT BELOW (U+0316)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u0340', ""); // COMBINING GRAVE TONE MARK (U+0340)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('‘', "'"); // LEFT SINGLE QUOTATION MARK (U+2018)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('’', "'"); // RIGHT SINGLE QUOTATION MARK (U+2019)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('‚', ","); // SINGLE LOW-9 QUOTATION MARK (U+201A)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('‛', "'"); // SINGLE HIGH-REVERSED-9 QUOTATION MARK (U+201B)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('“', "\""); // LEFT DOUBLE QUOTATION MARK (U+201C)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('”', "\""); // RIGHT DOUBLE QUOTATION MARK (U+201D)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('„', ",,"); // DOUBLE LOW-9 QUOTATION MARK (U+201E)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('‟', "\""); // DOUBLE HIGH-REVERSED-9 QUOTATION MARK (U+201F)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('❛', "'"); // HEAVY SINGLE TURNED COMMA QUOTATION MARK ORNAMENT (U+275B)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('❜', "'"); // HEAVY SINGLE COMMA QUOTATION MARK ORNAMENT (U+275C)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('❝', "\""); // HEAVY DOUBLE TURNED COMMA QUOTATION MARK ORNAMENT (U+275D)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('❞', "\""); // HEAVY DOUBLE COMMA QUOTATION MARK ORNAMENT (U+275E)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u275F', ","); // HEAVY LOW SINGLE COMMA QUOTATION MARK ORNAMENT (U+275F)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('\u2760', ",,"); // HEAVY LOW DOUBLE COMMA QUOTATION MARK ORNAMENT (U+2760)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('‹', "'"); // SINGLE LEFT-POINTING ANGLE QUOTATION MARK (U+2039)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('›', "'"); // SINGLE RIGHT-POINTING ANGLE QUOTATION MARK (U+203A)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('«', "\""); // LEFT-POINTING DOUBLE ANGLE QUOTATION MARK (U+00AB)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('»', "\""); // RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK (U+00BB)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('❮', "\""); // HEAVY LEFT-POINTING ANGLE QUOTATION MARK ORNAMENT (U+276E)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('❯', "\""); // HEAVY RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT (U+276F)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('〝', "\""); // REVERSED DOUBLE PRIME QUOTATION MARK (U+301D)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('〞', "\""); // DOUBLE PRIME QUOTATION MARK (U+301E)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('〟', ",,"); // LOW DOUBLE PRIME QUOTATION MARK (U+301F)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('＂', "\""); // FULLWIDTH QUOTATION MARK (U+FF02)

		// Ligature (AE, OE, etc.)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('œ', "oe"); // LATIN SMALL LIGATURE OE (U+0153)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('Œ', "OE"); // LATIN CAPITAL LIGATURE OE (U+0152)

		// Others
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('℃', "°C"); // DEGREE CELSIUS (U+2103)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('℉', "°F"); // DEGREE FAHRENHEIT (U+2109)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('K', "K"); // KELVIN SIGN (U+212A)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('α', "a"); // GREEK SMALL LETTER ALPHA (U+03B1)    => Hex: CE B1 (the "real" alpha)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('ɑ', "a"); // LATIN SMALL LETTER ALPHA (U+0251)    => Hex: C9 91
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('⍺', "a"); // APL FUNCTIONAL SYMBOL ALPHA (U+237A) => Hex: E2 8D BA
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('β', "B"); // GREEK SMALL LETTER BETA (U+03B2)     => Hex: CE B2
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('•', "*"); // BULLET (U+2022)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('…', "..."); // HORIZONTAL ELLIPSIS (U+2026)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('❨', "("); // MEDIUM LEFT PARENTHESIS ORNAMENT (U+2768)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('❩', ")"); // MEDIUM RIGHT PARENTHESIS ORNAMENT (U+2769)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('❪', "("); // MEDIUM FLATTENED LEFT PARENTHESIS ORNAMENT (U+276A)
		UNICODE_TO_ISO_8859_1_CONVERSION_MAP.put('❫', ")"); // MEDIUM FLATTENED RIGHT PARENTHESIS ORNAMENT (U+276B)


		// ***********************
		// * ISO 8859-1 to ASCII *
		// ***********************

		// Map format:
		// Key: ISO 8859-1 char
		// Value: ASCII equivalence

		// Superscript (the ones' supported by ISO 8859-1 but not by ASCII)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('¹', "^1"); // SUPERSCRIPT ONE (U+00B9)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('²', "^2"); // SUPERSCRIPT TWO (U+00B2)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('³', "^3"); // SUPERSCRIPT THREE (U+00B3)

		// ISO 8859-1 spaces
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('\u00A0', " "); // NO-BREAK SPACE (U+00A0)

		// ISO 8859-1 quotes and coma
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('´', "'"); // ACUTE ACCENT (U+00B4)

		// Most common accentuated characters (They are not common in English but sometime used for foreign names)
		// A
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('à', "a"); // LATIN SMALL LETTER A WITH GRAVE (U+00E0)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('À', "A"); // LATIN CAPITAL LETTER A WITH GRAVE (U+00C0)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('â', "a"); // LATIN SMALL LETTER A WITH CIRCUMFLEX (U+00E2)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('Â', "A"); // LATIN CAPITAL LETTER A WITH CIRCUMFLEX (U+00C2)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('ä', "a"); // LATIN SMALL LETTER A WITH DIAERESIS (U+00E4)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('Ä', "A"); // LATIN CAPITAL LETTER A WITH DIAERESIS (U+00C4)
		// E
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('é', "e"); // LATIN SMALL LETTER E WITH ACUTE (U+00E9)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('É', "E"); // LATIN CAPITAL LETTER E WITH ACUTE (U+00C9)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('è', "e"); // LATIN SMALL LETTER E WITH GRAVE (U+00E8)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('È', "E"); // LATIN CAPITAL LETTER E WITH GRAVE (U+00C8)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('ê', "e"); // LATIN SMALL LETTER E WITH CIRCUMFLEX (U+00EA)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('Ê', "E"); // LATIN CAPITAL LETTER E WITH CIRCUMFLEX (U+00CA)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('ë', "e"); // LATIN SMALL LETTER E WITH DIAERESIS (U+00EB)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('Ë', "E"); // LATIN CAPITAL LETTER E WITH DIAERESIS (U+00CB)
		// I
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('ì', "i"); // LATIN SMALL LETTER I WITH GRAVE (U+00EC)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('Ì', "I"); // LATIN CAPITAL LETTER I WITH GRAVE (U+00CC)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('î', "i"); // LATIN SMALL LETTER I WITH CIRCUMFLEX (U+00EE)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('Î', "I"); // LATIN CAPITAL LETTER I WITH CIRCUMFLEX (U+00CE)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('ï', "i"); // LATIN SMALL LETTER I WITH DIAERESIS (U+00EF)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('Ï', "I"); // LATIN CAPITAL LETTER I WITH DIAERESIS (U+00CF)
		// O
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('ò', "o"); // LATIN SMALL LETTER O WITH GRAVE (U+00F2)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('Ò', "O"); // LATIN CAPITAL LETTER O WITH GRAVE (U+00D2)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('ô', "o"); // LATIN SMALL LETTER O WITH CIRCUMFLEX (U+00F4)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('Ô', "O"); // LATIN CAPITAL LETTER O WITH CIRCUMFLEX (U+00D4)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('ö', "o"); // LATIN SMALL LETTER O WITH DIAERESIS (U+00F6)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('Ö', "O"); // LATIN CAPITAL LETTER O WITH DIAERESIS (U+00D6)
		// U
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('ù', "u"); // LATIN SMALL LETTER U WITH GRAVE (U+00F9)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('Ù', "U"); // LATIN CAPITAL LETTER U WITH GRAVE (U+00D9)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('û', "u"); // LATIN SMALL LETTER U WITH CIRCUMFLEX (U+00FB)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('Û', "U"); // LATIN CAPITAL LETTER U WITH CIRCUMFLEX (U+00DB)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('ü', "u"); // LATIN SMALL LETTER U WITH DIAERESIS (U+00FC)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('Ü', "U"); // LATIN CAPITAL LETTER U WITH DIAERESIS (U+00DC)
		// C
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('ç', "c"); // LATIN SMALL LETTER C WITH CEDILLA (U+00E7)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('Ç', "C"); // LATIN CAPITAL LETTER C WITH CEDILLA (U+00C7)
		// N
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('ñ', "n"); // LATIN SMALL LETTER N WITH TILDE (U+00F1)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('Ñ', "N"); // LATIN CAPITAL LETTER N WITH TILDE (U+00D1)
		// Ligature (AE, OE, etc.)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('æ', "ae"); // LATIN SMALL LETTER AE (U+00E6)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('Æ', "AE"); // LATIN CAPITAL LETTER AE (U+00C6)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('ӕ', "ae"); // CYRILLIC SMALL LIGATURE A IE (U+04D5)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('Ӕ', "AE"); // CYRILLIC CAPITAL LIGATURE A IE (U+04D4)

		// Others
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('§', " section "); // SECTION SIGN (U+00A7)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('©', "(C)"); // COPYRIGHT SIGN (U+00A9)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('®', "(R)"); // REGISTERED SIGN (U+00AE)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('°', " degree "); // DEGREE SIGN (U+00B0)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('±', "+/-"); // PLUS-MINUS SIGN (U+00B1)
		ISO_8859_1_TO_ASCII_CONVERSION_MAP.put('µ', "u"); // MICRO SIGN (U+00B5)
	}

	// Auto-generated pattern (generated at compile time) containing every characters (key) of the map.
	// It look like this: "[ʰ⁰¹²³⁴⁵...]"
	private static final Pattern UNICODE_TO_ISO_8859_1_CONVERSION_PATTERN = Pattern.compile("[" + UnicodeUtils.join(UNICODE_TO_ISO_8859_1_CONVERSION_MAP.keySet()) + "]");
	private static final Pattern ISO_8859_1_TO_ASCII_CONVERSION_PATTERN = Pattern.compile("[" + UnicodeUtils.join(ISO_8859_1_TO_ASCII_CONVERSION_MAP.keySet()) + "]");

	public static String toISO_8859_1(String unicodeStr) {
		if (StringUtils.isEmpty(unicodeStr)) {
			return null;
		}
		String isoStr = UnicodeUtils.convertString(unicodeStr, UNICODE_TO_ISO_8859_1_CONVERSION_PATTERN, UNICODE_TO_ISO_8859_1_CONVERSION_MAP);
		if (StringUtils.isEmpty(isoStr)) {
			return null;
		}

		// Substitute any non ISO 8859-1 characters left.
		// NOTE: It would probably be more efficient to do replaceAll("[^\\x00-\\xFF]", "?"), but the
		//     Apache Imaging library use the following conversion to check if the String is valid,
		//     so it's safer to do the same.
		try {
			// Replace non ISO-8859-1 characters with a "?"
			return new String(isoStr.getBytes("ISO-8859-1"), "ISO-8859-1");
		} catch (UnsupportedEncodingException ex) {
			LOGGER.log(Level.SEVERE, "Unsupported ISO-8859-1 encoding! Can not convert String.", ex);
		}
		// Fallback - This can only happen if the conversion to "ISO-8859-1" failed (it will never happen)
		return UnicodeUtils.convertString(isoStr, ISO_8859_1_TO_ASCII_CONVERSION_PATTERN, ISO_8859_1_TO_ASCII_CONVERSION_MAP);
	}

	public static String toASCII(String unicodeStr) {
		if (StringUtils.isEmpty(unicodeStr)) {
			return null;
		}
		String isoStr = UnicodeUtils.convertString(unicodeStr, UNICODE_TO_ISO_8859_1_CONVERSION_PATTERN, UNICODE_TO_ISO_8859_1_CONVERSION_MAP);
		if (StringUtils.isEmpty(isoStr)) {
			return null;
		}
		return UnicodeUtils.convertString(isoStr, ISO_8859_1_TO_ASCII_CONVERSION_PATTERN, ISO_8859_1_TO_ASCII_CONVERSION_MAP).replaceAll("[^\\x00-\\x7F]", "?");
	}

	public static String join(Collection<Character> characters) {
		return join(characters, null);
	}
	public static String join(Collection<Character> characters, String glue) {
		StringBuilder sb = new StringBuilder();
		for (Character character : characters) {
			if (glue != null && sb.length() > 0) {
				sb.append(glue);
			}
			sb.append(character);
		}
		return sb.toString();
	}

	private static String convertString(String str, Pattern charactersPattern, Map<Character, String> conversionMap) {
		if (StringUtils.isEmpty(str)) {
			return null;
		}

		// Substitute characters from the ISO 8859-1 map.
		Matcher matcher = charactersPattern.matcher(str);
		StringBuffer sb = new StringBuffer();
		while (matcher.find()) {
			String found = matcher.group();
			if (found.length() == 1) {
				matcher.appendReplacement(sb, conversionMap.get(found.charAt(0)));
			}
		}
		matcher.appendTail(sb);
		return sb.toString();
	}
}
